{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "# 利用用户行为数据\n",
    "如何了解一个人呢？\n",
    "\n",
    "**通过用户留下的文字和行为了解用户兴趣和需求。**\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "实现个性化推荐的最理想情况是用户在注册的时候主动告知其喜欢什么。\n",
    "\n",
    "3个缺点：\n",
    "\n",
    "1.  现在的自然语言理解技术很难理解用户用来描述兴趣的自然语言；\n",
    "2.  用户的兴趣是不断变化的；\n",
    "3.  很多时候用户并不知道自己喜欢什么，或者很难用语言描述。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "**基于用户行为分析的推荐算法是个性化推荐系统的重要算法，学术界一般将这种类型的算法称为协同过滤<sup><a id=\"fnr.1\" class=\"footref\" href=\"#fn.1\">1</a></sup>算法。**\n",
    "**用户行为数据在网站上最简单的存在形式就是日志。**\n",
    "\n",
    "-   原始日志（raw log）\n",
    "-   会话日志（session log）\n",
    "\n",
    "    将多种原始日志按照用户行为汇总，每个会话表示一次用户行为和对应的服务。\n",
    "\n",
    "-   展示日志（impression log）\n",
    "\n",
    "    搜索引擎和搜索广告系统服务为每次查询生成的，记录了查询和返回结果。\n",
    "\n",
    "-   点击日志（click log）\n",
    "\n",
    "    用户点击了某个返回结果的点击信息。\n",
    "\n",
    "一个并行程序会周期性地归并展示日志和点击日志，得到的会话日志中每个消息是一个用户提交的查询、得到的结果以及点击。\n",
    "\n",
    "推荐系统和电子商务网站也会汇总原始日志生成描述用户行为的会话日志。会话日志通常存储在分布式数据仓库中，这些日志记录了用户的各种行为。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "用户行为在个性化推荐系统中一般分为两种：\n",
    "-   **显性反馈行为（explicit feedback）:** 用户明确表示对物品喜好的行为。主要方式就是 **评分** 和 **喜欢/不喜欢** 。\n",
    "\n",
    "-   **隐性反馈行为（implicit feedback）:** 不能明确反应用户喜好的行为。最具代表性的就是 **页面浏览行为** 。\n",
    "\n",
    "|          | 显性反馈数据 | 隐性反馈数据   |\n",
    "|----------|--------------|----------------|\n",
    "| 用户兴趣 | 明确         | 不明确         |\n",
    "| 数量     | 较少         | 庞大           |\n",
    "| 存储     | 数据库       | 分布式文件系统 |\n",
    "| 实时读取 | 实时         | 有延迟         |\n",
    "| 正负反馈 | 都有         | 只有正反馈     |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "按照反馈的方向分，可分为：\n",
    "-   **正反馈:** 用户的行为倾向于指用户喜欢该物品。\n",
    "-   **负反馈:** 用户的行为倾向于指用户不喜欢该物品。\n",
    "\n",
    "在显性反馈中，很容易区分一个用户行为是正反馈还是负反馈，在隐性反馈行为中，相对比较难以确定。\n",
    "\n",
    "|              | 显性反馈                   | 隐性反馈                               |\n",
    "|--------------|----------------------------|----------------------------------------|\n",
    "| 视频网站     | 用户对视频的评分           | 用户观看视频的日志、浏览视频页面的日志 |\n",
    "| 电子商务网站 | 用户对商品的评分           | 购买日志、浏览日志                     |\n",
    "| 门户网站     | 用户对新闻的评分           | 阅读新闻的日志                         |\n",
    "| 音乐网站     | 用户对音乐/歌手/专辑的评分 | 听歌的日志                             |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "一种统一的表示用户行为的方式，它将一个用户行为表示为6部分，即产生行为的用户和行为的对象、行为的种类、产生行为的上下文、行为的内容和权重。\n",
    "\n",
    "| 类型             | 标识             | 解释                                                                                               |\n",
    "|------------------|------------------|----------------------------------------------------------------------------------------------------|\n",
    "| 产生行为的用户   | user id          | 产生行为的用户的唯一标识                                                                           |\n",
    "| 行为的对象       | item id          | 产生行为的对象的唯一标识                                                                           |\n",
    "| 行为的种类       | behavior type    | 行为的种类（比如是购买还是浏览）                                                                   |\n",
    "| 产生行为的上下文 | context          | 产生行为的上下文，包括时间和地点等                                                                 |\n",
    "| 行为的权重       | behavior weight  | 行为的权重（如果是观看视频的行为，那么这个权重可以是观看时长；如果是打分行为，这个权重可以是分数） |\n",
    "| 行为的内容       | behavior content | 行为的内容（如果是评论行为，那么就是评论的文本；如果是打标签的行为，就是标签）                     |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "不同的数据集包含不同的行为，目前比较有代表性的数据集有下面几个。\n",
    "-   **无上下文信息的隐性反馈数据集**\n",
    "\n",
    "    每一条行为记录仅仅包含用户ID和物品ID。\n",
    "\n",
    "    Book-Crossing数据集。<sup><a id=\"fnr.2\" class=\"footref\" href=\"#fn.2\">2</a></sup>\n",
    "\n",
    "-   无上下文信息的显性反馈数据集\n",
    "\n",
    "    每一条记录包含用户ID、物品ID和用户对物品的评分。\n",
    "\n",
    "-   有上下文信息的隐性反馈数据集\n",
    "\n",
    "    每一条记录包含用户ID、物品ID和用户对物品产生行为的时间戳。\n",
    "\n",
    "    Lastfm数据集。<sup><a id=\"fnr.3\" class=\"footref\" href=\"#fn.3\">3</a></sup>\n",
    "\n",
    "-   有上下文信息的显性反馈数据集\n",
    "\n",
    "    每一条记录包含用户ID、物品ID、用户对物品的评分和评分行为发生的时间戳。\n",
    "\n",
    "    Netflix Prize数据集。<sup><a id=\"fnr.4\" class=\"footref\" href=\"#fn.4\">4</a></sup>\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org7a972d7\"></a>\n",
    "## 用户行为分析\n",
    "\n",
    "本节将介绍用户行为数据中蕴含的一般规律，这些规律是存在于很多网站中的普遍规律。\n",
    "\n",
    "\n",
    "<a id=\"org4a0a610\"></a>\n",
    "\n",
    "### 用户活跃度和物品流行度的分布\n",
    "\n",
    "互联网上的很多数据分布都满足一种称为Power Law的分布，在互联网领域也称长尾分布。\n",
    "\n",
    "用户行为数据也蕴含这种规律。令 $f_u(k)$ 为对 $k$ 个物品产生过行为的用户数，令 $f_i(k)$ 为被 $k$ 个用户产生过行为的物品数， $f_u(k)$ 和 $f_i(k)$ 都满足长尾分布：\n",
    "\n",
    "\\begin{equation}\n",
    "\\begin{split}\n",
    "& f_i(k) = \\alpha_i k^{\\beta_i} \\\\\n",
    "& f_u(k) = \\alpha_u k^{\\beta_u} \\\\\n",
    "\\end{split} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "可选择 Delicious 和 CiteULike 数据集进行分析。\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org4056928\"></a>\n",
    "### 用户活跃度和物品流行度的关系\n",
    "\n",
    "一般认为，新用户倾向于浏览热门的物品，因其对网站还不熟悉，而老用户会逐渐开始浏览冷门的物品。\n",
    "\n",
    "**用户越活跃，越倾向于浏览冷门的物品。**\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org5862f10\"></a>\n",
    "## 协同过滤算法\n",
    "\n",
    "仅仅基于用户行为数据设计的推荐算法一般称为协同过滤算法。\n",
    "\n",
    "协同过滤算法中有很多方法，比如基于邻域的方法（neighborhood-based）、隐语义模型（latent factor model）等。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"orga1c1e8a\"></a>\n",
    "### 基于邻域（neighborhood-based）的算法\n",
    "\n",
    "最著名、在业界得到最广泛应用的算法是基于邻域的方法，主要包括：\n",
    "\n",
    "-   **基于用户的协同过滤算法:** 给用户推荐和其兴趣相似的其他用户喜欢的物品。\n",
    "-   **基于物品的协同过滤算法:** 给用户推荐和其之前喜欢的物品相似的物品。\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"orga0cf053\"></a>\n",
    "### 实验设计和算法评测\n",
    "\n",
    "本节将通过离线实验方法评测提到的算法。\n",
    "\n",
    "首先介绍用到的数据集，然后介绍采用的实验方法和评测指标。\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"orga702f41\"></a>\n",
    "#### 数据集\n",
    "本章采用GroupLens提供的MovieLens数据集介绍和评测各种算法。 MovieLens数据集有3个不同的版本，本章选用中等大小的数据集。该数据集包含6000多用户对4000多部电影的100万条评分。该数据集是一个评分数据集，用户可以给电影评5个不同等级的分数(1~5分)。本章着重研究隐反馈数据集中的TopN推荐问题，因此忽略了数据集中的评分记录。也就是说，TopN推荐的任务是预测用户会不会对某部电影评分，而不是预测用户在准备对某部电影评分的前提下会给电影评多少分。\n",
    "\n",
    "加载数据：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "autoscroll": false,
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "outputs": [],
   "source": [
    "def load_movielens(path='./movielens/ml-100k'):\n",
    "    # get movie titles\n",
    "    movies = {}\n",
    "    for line in open(path + '/u.item', encoding='latin-1'):\n",
    "        id, title = line.split('|')[0:2]\n",
    "        movies[id] = title\n",
    "    # load data\n",
    "    prefs = {}\n",
    "    for line in open(path + '/u.data', encoding='latin-1'):\n",
    "        user, movieid, rating, ts = line.split('\\t')\n",
    "        prefs.setdefault(user, {})\n",
    "        prefs[user][movies[movieid]] = float(rating)\n",
    "    return prefs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "autoscroll": false,
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "outputs": [],
   "source": [
    "prefs = load_movielens()\n",
    "print(prefs['87'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org8ba2fe4\"></a>\n",
    "#### 实验设计\n",
    "\n",
    "协同过滤算法的离线实验一般如下设计。\n",
    "\n",
    "1.  将用户行为数据集按照均匀分布随机分成 $M$ 份(例如取 $M=8$ )，挑选一份作为测试集，将剩下的 $M-1$ 份作为训练集。\n",
    "2.  在训练集上建立用户兴趣模型，并在测试集上对用户行为进行预测，统计出相应的评测指标。为了保证评测指标并不是过拟合的结果，需要进行 $M$ 次实验，并且每次都使用不同的测试集。\n",
    "3.  将 $M$ 次实验测出的评测指标的平均值作为最终的评测指标。\n",
    "\n",
    "下面的代码描述了将数据集随机分成训练集和测试集的过程：\n",
    "\n",
    "```python\n",
    "def split_data(data, M, k, seed):\n",
    "    test = []\n",
    "    train = []\n",
    "    random.seed(seed)\n",
    "    for user, item in data:\n",
    "        if random.randint(0, M) == k:\n",
    "            test.append([user, item])\n",
    "        else:\n",
    "            train.append([user, item])\n",
    "    return train, test\n",
    "```\n",
    "\n",
    "每次实验选取不同的 $k(0≤k≤M-1)$ 和相同的随机数种子seed，进行 $M$ 次实验就可以得到 $M$ 个不同的训练集和测试集，然后分别进行实验，用 $M$ 次实验的平均值作为最后的评测指标。这样做主要是防止某次实验的结果是过拟合的结果(over fitting)，但如果数据集够大，模型够简单，为了快速通过离线实验初步地选择算法，也可以只进行一次实验。\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org1e73bfe\"></a>\n",
    "#### 评测指标\n",
    "\n",
    "对用户 $u$ 推荐 $N$ 个物品（记为 $R(u)$ ），令用户 $u$ 在测试集上喜欢的物品集合为 $T(u)$ ，然后可以通过准确率/召回率评测推荐算法的精度:\n",
    "\n",
    "\\begin{equation}\n",
    "\\begin{split}\n",
    "& Recall = \\frac{\\sum_u |R(u) \\cap T(u)|}{\\sum_u |T(u)|} \\\\\n",
    "& Precision = \\frac{\\sum_u |R(u) \\cap T(u)|}{\\sum_u |R(u)|} \\\\\n",
    "\\end{split} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "召回率描述有多少比例的用户——物品评分记录包含在最终的推荐列表中，而准确率描述最终的推荐列表中有多少比例是发生过的用户——物品评分记录。\n",
    "\n",
    "下面两段代码给出了召回率和准确率的计算方法。\n",
    "\n",
    "```python\n",
    "def recall(train, test, N, GetRecommendation=None):\n",
    "    hit = 0\n",
    "    all = 0\n",
    "    for user in train.keys():\n",
    "        tu = test[user]\n",
    "        rank = GetRecommendation(user, N)\n",
    "        for item, pui in rank:\n",
    "            if item in tu:\n",
    "                hit += 1\n",
    "        all += len(tu)\n",
    "    return hit / (all * 1.0)\n",
    "```\n",
    "\n",
    "```python\n",
    "def precision(train, test, N, GetRecommendation=None):\n",
    "    hit = 0\n",
    "    all = 0\n",
    "    for user in train.keys():\n",
    "        tu = test[user]\n",
    "        rank = GetRecommendation(user, N)\n",
    "        for item, pui in rank:\n",
    "            if item in tu:\n",
    "                hit += 1\n",
    "        all += N\n",
    "    return hit / (all * 1.0)\n",
    "```\n",
    "\n",
    "下面的公式给出了最简单的覆盖率定义，覆盖率反映了推荐算法发掘长尾的能力，覆盖率越高，说明推荐算法越能够将长尾中的物品推荐给用户。\n",
    "\n",
    "\\begin{equation}\n",
    "Coverage = \\frac{U_{u \\in U} R(u)}{|I|} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有的物品都被推荐给至少一个用户，那么覆盖率就是100%。如下代码可以用来计算推荐算法的覆盖率：\n",
    "\n",
    "```python\n",
    "def coverage(train, test, N, GetRecommendation=None):\n",
    "    recommend_items = set()\n",
    "    all_items = set()\n",
    "    for user in train.keys():\n",
    "        for item in train[user].keys():\n",
    "            all_items.add(item)\n",
    "        rank = GetRecommendation(user, N)\n",
    "        for item, pui in rank:\n",
    "            recommend_items.add(item)\n",
    "    return len(recommend_items) / (len(all_items) * 1.0)\n",
    "```\n",
    "\n",
    "最后，用推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门，说明推荐的新颖度较低，否则说明推荐结果比较新颖。\n",
    "\n",
    "```python\n",
    "def popularity(train, test, N, GetRecommendation=None):\n",
    "    item_popularity = dict()\n",
    "    for user, items in train.items():\n",
    "        for item in items.keys():\n",
    "            if item not in item_popularity:\n",
    "                item_popularity[item] = 0\n",
    "            item_popularity[item] += 1\n",
    "    ret = 0\n",
    "    n = 0\n",
    "    for user in train.keys():\n",
    "        rank = GetRecommendation(user, N)\n",
    "        for item, pui in rank:\n",
    "            ret += math.log(1 + item_popularity[item])\n",
    "            n += 1\n",
    "    ret /= n * 1.0\n",
    "    return ret\n",
    "```\n",
    "\n",
    "在计算平均流行度时对每个物品的流行度取对数，这是因为物品的流行度分布满足长尾分布，在取对数后，流行度的平均值更加稳定。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "## 脚注\n",
    "\n",
    "<sup><a id=\"fn.1\" class=\"footnum\" href=\"#fnr.1\">1</a></sup> 协同过滤就是指用户可以齐心协力，通过不断地和网站互动，使自己的推荐列表能够不断过滤掉自己不感兴趣的物品，从而越来越满足自己的需求。\n",
    "\n",
    "<sup><a id=\"fn.2\" class=\"footnum\" href=\"#fnr.2\">2</a></sup> 参见“Book-Crossing Dataset”，地址为<http://www.informatik.uni-freiburg.de/~cziegler/BX/>。\n",
    "\n",
    "<sup><a id=\"fn.3\" class=\"footnum\" href=\"#fnr.3\">3</a></sup> 参见<http://www.dtic.upf.edu/~ocelma/MusicRecommendationDataset/lastfm-1K.html>。\n",
    "\n",
    "<sup><a id=\"fn.4\" class=\"footnum\" href=\"#fnr.4\">4</a></sup> 参见<http://netflixprize.com/>。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  },
  "name": "4-use-user-behavior-data.ipynb",
  "toc": {
   "colors": {
    "hover_highlight": "#ddd",
    "running_highlight": "#FF0000",
    "selected_highlight": "#ccc"
   },
   "moveMenuLeft": true,
   "nav_menu": {
    "height": "123px",
    "width": "252px"
   },
   "navigate_menu": true,
   "number_sections": false,
   "sideBar": true,
   "threshold": 4,
   "toc_cell": false,
   "toc_section_display": "block",
   "toc_window_display": false,
   "widenNotebook": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
